/**
 * @file filemeta.cpp
 * @author smdsbz (69406681@qq.com)
 * @version 0.1
 * @date 2022-01-06
 *
 * Testing file create / delete metadata ops performance.
 */

#include <iostream>
#include <iomanip>
#include <boost/log/trivial.hpp>
#include <sstream>
#include <filesystem>
#include <fstream>
#include <unordered_map>
#include <vector>
#include <chrono>
#include <cstdlib>
#include <boost/program_options.hpp>
#include <boost/property_tree/json_parser.hpp>

#include <fcntl.h>

using namespace std;

constexpr char DIR_PREFIX[] = "cephfsbench-test-dir-";
constexpr char FILE_PREFIX[] = "test-file-";


int main(const int argc, const char **argv)
{
    /* parameter */

    size_t num_files, num_dirs;
    bool precreate_directories, interleave, cleanup;
    filesystem::path ceph_bin_dir, cephfs_mount_point;
    string mds_daemon;
    string client_daemon;

    namespace po = boost::program_options;
    po::options_description opt_desc;
    opt_desc.add_options()
        ("num-files", po::value(&num_files)->default_value(1e5),
            "Total number of files, defaults to 1e5.")
        ("num-dirs", po::value(&num_dirs)->default_value(1),
            "Total number of directories, defaults to 1.")
        ("precreate-directories", po::value(&precreate_directories)->default_value(true),
            "Create directories before workload. Currently only true is supported.")
        ("interleave", po::value(&interleave)->default_value(true),
            "Place file into directories in an interleaving manner, defaults to true.")
        ("cleanup", po::value(&cleanup)->default_value(true),
            "Cleanup after run, defaults to true.")
        ("ceph-bin-dir", po::value(&ceph_bin_dir)->default_value("/mnt/zxg/ceph/build/bin/"),
            "Ceph binary directory, defaults to /mnt/zxg/ceph/build/bin/.")
        ("cephfs-mount-point", po::value(&cephfs_mount_point)->default_value("/data/cephfs/"),
            "CephFS mount point, defaults to /data/cephfs/.")
        ("mds-daemon", po::value(&mds_daemon)->default_value("mds.a"),
            "MDS daemon name, defaults to mds.a.")
    ;
    po::variables_map vm;
    po::store(po::parse_command_line(argc, argv, opt_desc), vm);
    po::notify(vm);

    if (!precreate_directories)
        throw std::runtime_error("Currently must create directory beforehand");

    const auto ceph_bin = ceph_bin_dir / "ceph";
    if (!filesystem::exists(ceph_bin))
        throw std::runtime_error("Cannot find ceph binary");
    if (!filesystem::exists(cephfs_mount_point))
        throw std::runtime_error("Cannot find CephFS mount point");

    /* get client daemon name */
    {
        constexpr char TMPFILE[] = "/tmp/cephfsbench-test.client";
        ostringstream cli;
        cli << "ls /var/run/ceph/ceph-client.admin.$(pgrep ceph-fuse).* > " << TMPFILE;
        (void)! std::system(cli.str().c_str());
        filesystem::path client_sock;
        {
            ifstream cf(TMPFILE);
            cf >> client_sock;
        }
        client_daemon = client_sock.stem().string().substr(sizeof("ceph-")-1);
        filesystem::remove(TMPFILE);
    }


    /* setup */

    /**
     * Generate directory name
     * @param di directory index
     * @return directory name
     */
    auto gen_dir_name = [](size_t di) -> string
    {
        ostringstream s;
        s << DIR_PREFIX << di;
        return s.str();
    };
    /**
     * Generate test file path, relative.
     * @param fi file index
     * @return file path
     */
    auto get_file_path = [&](size_t fi) -> filesystem::path
    {
        ostringstream s;
        s << gen_dir_name(interleave ? fi % num_dirs : fi / num_dirs) << "/"
            << FILE_PREFIX << fi;
        return s.str();
    };
    vector<filesystem::path> dirset(num_dirs);
    for (unsigned d = 0; d < num_dirs; d++) {
        auto &dirpath = dirset[d];
        dirpath = cephfs_mount_point / gen_dir_name(d);
        filesystem::create_directory(dirpath);
    }
    vector<filesystem::path> fileset(num_files);
    for (size_t f = 0; f < num_files; f++) {
        auto &filepath = fileset[f];
        filepath = cephfs_mount_point / get_file_path(f);
    }

    /* timing */

    /**
     * Reset MDS performance counters
     * @param daemon
     */
    auto perf_reset = [&](const string &daemon)
    {
        ostringstream cli;
        cli << ceph_bin << " daemon " << daemon << " perf reset all"
            << " > /dev/null 2>&1";
        (void)! std::system(cli.str().c_str());
    };
    auto perf_reset_all = [&]
    {
        perf_reset(mds_daemon);
        perf_reset(client_daemon);
    };
    /**
     * Read MDS performance counters
     * @param daemon
     * @param category perf entry path, defaults to "", i.e. all
     * @return mds_server counters
     */
    auto get_perf = [&](const string &daemon, const string &category = "")
    {
        constexpr char TMPFILE[] = "/tmp/cephfsbench-perf.json";
        ostringstream cli;
        cli << ceph_bin << " daemon " << daemon << " perf dump " << category
            << " 2>/dev/null > " << TMPFILE;
        (void)! std::system(cli.str().c_str());

        namespace pt = boost::property_tree;
        ifstream f(TMPFILE);
        pt::ptree tree;
        pt::read_json(f, tree);

        filesystem::remove(TMPFILE);
        if (category.empty())
            return tree;
        return tree.get_child(category);
    };
    /**
     * Print test result
     * @param name name of test
     * @param run total run time
     * @param count total entry count
     * @param perf mds_server tree
     */
    auto print_result = [&](
        const string &name,
        const std::chrono::nanoseconds &run, size_t count)
    {
        std::cout << std::left
            << std::setw(32) << "entry"
            << std::setw(12) << "avg lat(us)"
            << std::setw(12) << "count"
            << "\n" << string(32+12+12, '-')
            << std::endl;
        /**
         * @param entry name
         * @param avglat in microseconds
         * @param count
         */
        auto print_raw = [](const string &entry, double avglat, size_t count)
        {
            std::cout << std::left
                << std::setw(32) << entry
                << std::setw(12) << avglat
                << std::setw(12) << count
                << std::right << std::endl;
        };
        /**
         * @param entry path in dump JSON
         * @param min_count minimum count for the entry to be print out, defaults
         *      to 1, i.e. ignore inactive code path
         */
        auto print_entry = [&](
            const boost::property_tree::ptree &perf, const string &entry,
            size_t min_count = 1)
        {
            const auto v = perf.get_child(entry);
            const auto avgcount = v.get<size_t>("avgcount");
            if (avgcount < min_count)
                return;
            print_raw(entry, v.get<double>("avgtime") * 1e6, avgcount);
        };

        /* end-to-end metric */
        print_raw(name, (double)run.count() / count / 1e3, count);

        /* ceph-fuse client perf */
        {
            const auto perf = get_perf(client_daemon);
            print_entry(perf, "client.reply");
            print_entry(perf, "client.lat");
        }

        /* mds perf */
        {
            const auto perf = get_perf(mds_daemon);
            print_entry(perf, "mds.reply_latency");
            print_entry(perf, "mds_log.jlat");
            for (const auto &[k, v] : perf.get_child("mds_server")) {
                if (!k.starts_with("req_"))
                    continue;
                print_entry(perf, string("mds_server.") + k);
            }
        }

        std::cout << std::right << std::endl;
    };

    /* run create workload */
    {
        BOOST_LOG_TRIVIAL(info) << "running file create ...";
        perf_reset_all();
        const auto start = std::chrono::steady_clock::now();
        for (const auto &f : fileset) {
            int fd = creat(f.c_str(), 0);
            if (fd < 0)
                throw std::runtime_error(string("Failed to create file ") + f.string() + " with " + strerror(errno));
            close(fd);
        }
        const auto end = std::chrono::steady_clock::now();
        const auto run = end - start;

        print_result("creat(2)", run, fileset.size());
    }

    /* run delete workload */
    {
        BOOST_LOG_TRIVIAL(info) << "running file delete ...";
        perf_reset_all();
        const auto start = std::chrono::steady_clock::now();
        for (const auto &f : fileset) {
            int r = unlink(f.c_str());
            if (r < 0)
                throw std::runtime_error(string("Failed to delete file " + f.string()) + " with " + strerror(errno));
        }
        const auto end = std::chrono::steady_clock::now();
        const auto run = end - start;

        print_result("unlink(2)", run, fileset.size());
    }

    /* cleanup */
    BOOST_LOG_TRIVIAL(info) << "cleanup ...";
    for (const auto &d : dirset) {
        filesystem::remove_all(d);
    }

    BOOST_LOG_TRIVIAL(info) << "done!";
    return 0;
}
